package pyip.lib.javalearn.io.nio;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

/**
 * <b>写就绪</b><br>
 * 写就绪相对有一点特殊，一般来说，你不应该注册写事件。<br>
 * 写操作的就绪条件为底层缓冲区有空闲空间，而写缓冲区绝大部分时间都是有空闲空间的，所以当你注册写事件后，写操作一直是就绪的，选择处理线程会占用整个CPU资源。<br>
 * 所以，只有当你确实有数据要写时再注册写操作，并在写完以后马上取消注册。
 * @author yepeng
 * @date 2015年10月10日
 */
public class TcpServer implements Runnable {

    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    private RequestProcesser processer;

    private ByteBuffer writeBuf = ByteBuffer.allocate(10240);

    public TcpServer(int port) throws IOException {
        serverSocketChannel = ServerSocketChannel.open();
        selector = Selector.open();
        processer = new RequestProcesser(selector);

        serverSocketChannel.socket().bind(new InetSocketAddress(port));
        serverSocketChannel.configureBlocking(false);
        // 注册accept, ServerSocketChannel支持accept
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
    }

    @Override
    public void run() {
        System.out.println("TPCServer started");
        while (true) {
            try {
                try {
                    // 可以被wakeup interrupt及selected唤醒
                    // 返回的是selector.selectedKeys()新增的个数, 而不是有新的读/写/accept/connect就绪
                    // 比如remove了selector.selectedKeys()中的key, 但不处理这些key, selector.select()会返回相同的个数
                    if (selector.select() == 0) {
                        continue;
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                    continue;
                }

                for (Iterator<SelectionKey> it = selector.selectedKeys().iterator(); it.hasNext();) {
                    SelectionKey key = it.next();
                    it.remove();
                    if (key.isAcceptable() && key.isValid()) {
                        doServerSocketEvent(key);
                    } else if (key.isReadable() && key.isValid()) {
                        key.interestOps(key.interestOps() & ~SelectionKey.OP_READ);
                        doReadEvent(key);
                    } else if (key.isWritable() && key.isValid()) {
                        key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
                        doWriteEvent(key);
                    }
                }
            } catch (Throwable e) {
                e.printStackTrace();
            }
        }
    }

    // 注册SocketChannel读监听
    private void doServerSocketEvent(SelectionKey key) {
        SocketChannel client = null;
        ServerSocketChannel server = (ServerSocketChannel) key.channel();
        try {
            client = server.accept();
            if (client == null) {
                return;
            }
            client.configureBlocking(false);
            // client.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            client.register(selector, SelectionKey.OP_READ);
            client.socket().setOOBInline(true);
        } catch (IOException e) {
            try {
                client.close();
            } catch (IOException e1) {
                e1.printStackTrace();
            }
        }
    }

    // 读取请求数据
    private void doReadEvent(SelectionKey key) {
        processer.doRequest(key);
    }

    // 写响应数据, 并取消写监听
    private void doWriteEvent(SelectionKey key) {
        SocketChannel socketChannel = (SocketChannel) key.channel();
        byte[] data = "let me tell you a secret".getBytes();
        writeBuf.put(data);
        writeBuf.flip();
        try {
            while (writeBuf.hasRemaining()) {
                socketChannel.write(writeBuf);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        try {
            TcpServer server = new TcpServer(9999);
            new Thread(server).start();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}